<!DOCTYPE html>
<!-- https://github.com/OAIE/oaie-sketch -->
<head>
	<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'>
	<title>OAIE-Sketch</title>
</head>

<link rel='stylesheet' type='text/css' href='common.css' />
<link rel='stylesheet' type='text/css' href='sketch.css' />
<script src='common.js'></script>
<script src='rect.js'></script>
<script src='settings.js'></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.12.0/js-yaml.min.js'></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>

<!-- swagger editor experiment
<link href="https://editor.swagger.io/dist/swagger-editor.css" rel="stylesheet">
<script src="https://editor.swagger.io/dist/swagger-editor-bundle.js"> </script>
<style>
.swagger-editor { position: absolute; left: 0; top: 0; right: 70%; bottom: 0; }
.swagger-editor .Pane2 { display: none; }
.swagger-editor .SplitPane { height: 100% !important; }
.swagger-editor #ace-editor { height: 100vh !important; }
</style>
<script>
function initSwaggerEditor(content) {
	// https://github.com/swagger-api/swagger-editor/tree/master/dist
	// TODO: evaluate if this editor is even helpful to anyone.
	//       i would actually very much prefer a well-configured CodeMirror
	// TODO: observe changes
	// TODO: scroll to line
	// TODO: in-bind content
	// TODO: the editor bootstraps very slowly. will it help not to self-host?
	localStorage['swagger-editor-content'] = content;
	const editor = SwaggerEditorBundle({ dom_id: '#swaggerEditor' })
    window.editor = editor
	window.setTimeout(function() {
		// TODO: getting the editor somehow kills it ... why?
		//var editor = ace.edit('swaggerEditor');
		//editor.scrollToLine(50, true, true, function () {});
		//editor.gotoLine(50, 10, true);
	}, 1000);
}
</script>
<!-- -->

<body class='oaie sketch'>

<div id='app' :class='{ editorWide: editorWide }'>
	<div id='specEditor' v-if="!settings">
		<div v-for="(settingsKey, settingsName) in allSettings" class="settings" @click="selectSettings(settingsKey)">{{ settingsName }}</div>
		<div class="settings new" @click="newSettings()">&plus; Create a new spec</div>
	</div>
	<div id='specEditor' v-if="settings">
		<div class='editor'>
			<div class='measureHelper'>Text</div>
			<textarea wrap='off' v-model="settings.spec" @change='save(); editorChanged = true;' spellcheck="false"></textarea>
		</div>
		<span class='editorWideToggle' @click='editorWide = !editorWide'>{{ editorWide ? '&olt;' : '&ogt;' }}</span>
	</div>
	<div id='outputViewer' v-if="settings">
		<div class='menu'>
			<button @click='load' :class='{ changed: editorChanged }'>&rarr; update from editor</button>
			<button @click='updateInlineViz' :class='{ changed: positionsChanged }'>&larr; update inline viz</button>
			<!--<button @click='dump'>&equiv; dump to console</button>-->
			<button @click='addSchema'>&oplus; schema</button>
			<div v-if="spec && spec.info" class='title'>{{ spec.info.title }} <span class='version'>{{ spec.info.version }}</span></div>
		</div>
		<div id='output'>
			<a v-for='(connection, c) in connections'
				v-bind:class='["arrow", connection.type]'
				v-bind:source='connection.source'
				v-bind:target='connection.target'
				v-bind:id='"connection" + c'></a>
			<div v-bind:class='["operation", operation._method]'
				v-bind:id='operation._key'
				v-bind:oaie-key='operation._key'
				v-bind:style='{ left: getPositionLeft(operation._key), top: getPositionTop(operation._key) }'
				v-for='operation in operations'
				v-draggable>
				<div class='path'>
					<b>{{ operation._method }}</b>
					<span @click='selectPath("paths:/" + encodeURIComponent(operation._path) + ":/" + operation._method + ":")'>{{ operation._path }}</span>
				</div>
				<div class='buttonRow'>
					<button class="mini" @click="moveAllY(operation, -50)">&uarr;</button>
					<button class="mini" @click="moveAllY(operation, 50)">&darr;</button>
				</div>
				<div class='summary'><b>{{ operation.operationId }}</b> {{ operation.summary }}</div>
				<ul>
					<li v-for='parameter in operation.parameters'
						v-bind:class='{ required: parameter.required }'>
						<button>-</button>
						<span @click='selectPath("paths:/" + encodeURIComponent(operation._path) + ":/" + operation._method + ":/parameters:/- name: " + parameter.name, parameter.name)'>
							{{ parameter.name }} ({{ parameter.schema.type }})
						</span>
					</li>
					<li><button>+</button></li>
				</ul>
			</div>
			<div class='schema'
				v-if='spec && spec.components'
				v-for='(schema, schemaName) in spec.components.schemas'
				v-bind:id='"schema." + schemaName'
				v-bind:oaie-key='"schema." + schemaName'
				v-bind:style='{ left: getPositionLeft("schema." + schemaName), top: getPositionTop("schema." + schemaName) }'
				v-draggable>
				<h1 @click='selectPath("components:/schemas:/" + schemaName + ":")'>{{ schemaName }}</h1>
				<div class='description'>{{ schema.description }}</div>
				<ul>
					<li v-for='(property, propertyName) in schema.properties'>
						<button>-</button>
						<span @click='selectPath("components:/schemas:/" + schemaName + ":/properties:/" + propertyName + ":")'>
							{{ propertyName }} ({{ property.type ? property.type : property.$ref ? property.$ref.replace('#/components/schemas/', '&rarr;') : '' }})</li>
						</span>
					<li><button @click='addSchemaProperty(schema, schemaName)'>+</button></li>
				</ul>
			</div>
		</div>
	</div>
</div>

<div id="swaggerEditor"></div>

<script>
function init(config, i18n) {
	var settingsSystem = new Settings();
	var ml = i18n[settingsSystem.settings ? settingsSystem.settings.locale : "en"];
	
	var setLocale = function(locale) {
		if (settings) {
			settings.locale = locale;
			ml = i18n[settings.locale];
		}
		// we also need to tell vue the new object because we swapped the whole object out
		app.ml = ml;
	}
	
	// TODO: remove jQuery
	var arrowConnect = function(arrow, sourceEl, targetEl) {
		var source = new Rect(
			Math.round(sourceEl.style.left.replace(/[^\d\.]/g, '')),
			Math.round(sourceEl.style.top.replace(/[^\d\.]/g, '')),
			$(sourceEl).width(),
			$(sourceEl).height()
		);
		var target = new Rect(
			Math.round(targetEl.style.left.replace(/[^\d\.]/g, '')),
			Math.round(targetEl.style.top.replace(/[^\d\.]/g, '')),
			$(targetEl).width(),
			$(targetEl).height()
		);
		
		var start = source.findEnd(target.center());
		$(arrow).css('left', start.x);
		$(arrow).css('top', start.y);
		var end = target.findEnd(source.center());
		var dx = end.x - start.x;
		var dy = end.y - start.y;
		var d = Math.sqrt(dx * dx + dy * dy);
		$(arrow).css('width', d + 'px');
		var a = Math.atan2(dy, dx);
		$(arrow).css('transform', 'rotate(' + a + 'rad)');
	};
	
	var arrowsDirty = false;
	var connectArrows = function(el) {
		$('[source="' + el.id + '"],[target="' + el.id + '"]').each(function(i,o) {
			var source = document.getElementById($(o).attr('source'));
			var target = document.getElementById($(o).attr('target'));
			arrowConnect(o, source, target);
		});
		arrowsDirty = false;
	};
	
	var connectArrowsAll = function(el) {
		$('.arrow').each(function(i,o) {
			var source = document.getElementById($(o).attr('source'));
			var target = document.getElementById($(o).attr('target'));
			arrowConnect(o, source, target);
		});
	};
	
	Vue.directive('draggable', {
		inserted: function(el) {
			connectArrows(el);
			$(el).draggable({
				drag: function(e) {
					if (arrowsDirty == false) {
						arrowsDirty = true;
						// $nextTick seems to be always after the dragging, so we use a timer.
						window.setTimeout(() => { connectArrows(el); }, 50);
					}
				},
				stop: function(e) {
					connectArrows(el);
					var settings = settingsSystem.settings;
					var key = $(el).attr('oaie-key');
					if (settings.positions === undefined) settings.positions = {};
					if (settings.positions[key] === undefined) settings.positions[key] = {};
					settings.positions[key].x = Math.round(this.style.left.replace(/[^\d\.]/g, ''));
					settings.positions[key].y = Math.round(this.style.top.replace(/[^\d\.]/g, ''));
					app.positionsChanged = true;
					settingsSystem.storeSettings();
				}
			});
		}
	});
	
	/*var*/ app = new Vue({
		el: '#app',
		data: {
			allSettings: settingsSystem.allSettings,
			settings: settingsSystem.settings,
			config: config,
			i18n: i18n,
			ml: ml,
			activeExample: null,
			spec: { paths: [], components: { schemas: {}, }, },
			editorChanged: false,
			positionsChanged: false,
			editorWide: false,
		},
		computed: {
			operations: function() {
				var r = [];
				for (var p in this.spec.paths) {
					var path = this.spec.paths[p];
					for (var method in path) {
						var operation = path[method];
						operation._method = method;
						operation._path = p;
						operation._key = "operation." + operation._method + "." + operation._path;
						r.push(operation);
					}
				}
				return r;
			},
			connections: function() {
				var r = [];
				// TODO: array connections
				// TODO: find a more generic way than this
				//       just walk the whole tree and look for '$ref'?
				//       just walk from certain starting points
				// TODO: are there other connections?

				var noInArrows = {};
				for (var s in this.spec.components.schemas) {
					var schema = this.spec.components.schemas[s];
					if (!schema.description) continue;
					if (schema.description.indexOf("OAIE.noInArrows") >= 0)
						noInArrows[s] = true;
				}

				var getRef = function(o) {
					if (o['$ref'] === undefined) return null;
					var ref = o['$ref'];
					return ref.replace(/^#.*\//, '');
				}
				var getSchemaRef = function(o) {
					if (o.content === undefined ||
						o.content['application/json'] === undefined ||
						o.content['application/json'].schema === undefined)
						return null;
					return getRef(o.content['application/json'].schema);
				};
				var getArraySchemaRef = function(o) {
					if (o.content === undefined ||
						o.content['application/json'] === undefined ||
						o.content['application/json'].schema === undefined ||
						o.content['application/json'].schema.items === undefined)
						return null;
					return getRef(o.content['application/json'].schema.items);
				};

				if (!this.spec) return r;

				// connect paths to schemas
				for (var p in this.spec.paths) {
					var path = this.spec.paths[p];
					for (var method in path) {
						var operation = path[method];
						if (operation.requestBody !== undefined) {
							var ref = getSchemaRef(operation.requestBody);
							if (ref == null) continue;
							if (ref != null && !noInArrows[ref]) r.push({ source: "operation." + method + "." + p, target: 'schema.' + ref, type: 'request' });
						}
						for (var code in operation.responses) {
							var response = operation.responses[code];
							var ref = getSchemaRef(response);
							if (ref != null && !noInArrows[ref]) r.push({ source: "operation." + method + "." + p, target: 'schema.' + ref, type: 'response' });
							
							var ref = getArraySchemaRef(response);
							if (ref != null && !noInArrows[ref]) r.push({ source: "operation." + method + "." + p, target: 'schema.' + ref, type: 'array' });
						}
					}
				}
				
				// connect schemas to schemas
				for (var s in this.spec.components.schemas) {
					var schema = this.spec.components.schemas[s];
					for (var p in schema.properties) {
						var property = schema.properties[p];
						var ref = getRef(property);
						if (ref != null && !noInArrows[ref]) r.push({ source: 'schema.' + s, target: 'schema.' + ref, type: 'propertyObject'});
						// TODO: is this even a legal syntax?
						if (property.type == 'object') {
							var ref = getRef(property);
							if (ref != null) r.push({ source: 'schema.' + s, target: 'schema.' + ref, type: 'propertyObject'});
						}
						if (property.type == 'array') {
							if (property.items !== undefined) {
								var ref = getRef(property.items);
								if (ref != null && !noInArrows[ref]) r.push({ source: 'schema.' + s, target: 'schema.' + ref, type: 'propertyArray'});
							}
						}
					}
					if (schema.type == 'array') {
						if (schema.items !== undefined) {
							var ref = getRef(schema.items);
							if (ref != null && !noInArrows[ref]) r.push({ source: 'schema.' + s, target: 'schema.' + ref, type: 'array' });
						}
					}
				}
				return r;
			},
		},
		methods: {
			selectSettings(settingsKey) {
				settingsSystem.settings = settingsSystem.loadSettings(settingsKey);
				this.settings = settingsSystem.settings;
				if (window.initSwaggerEditor) initSwaggerEditor(this.settings.spec);
				app.load();
			}, 
			newSettings() {
				settingsSystem.settings = settingsSystem.getNewSettings();
				this.settings = settingsSystem.settings;
				app.load();
			},
			getPositionLeft: function(key) {
				if (this.settings.positions === undefined) return 0;
				if (this.settings.positions[key] === undefined) return 0;
				return this.settings.positions[key].x + 'px';
			},
			getPositionTop: function(key) {
				if (this.settings.positions === undefined) return 0;
				if (this.settings.positions[key] === undefined) return 0;
				return this.settings.positions[key].y + 'px';
			},
			save: function(e) {
				settingsSystem.storeSettings();
			},
			sortObject: function(object) {
				const sorted = {};
				Object.keys(object).sort().forEach(function(key) {
					sorted[key] = object[key];
				});
				return sorted;
			},
			selectPath: function(path, selectSubString) {
				var textarea = $('#specEditor textarea').get(0);
				var o = 0;
				var t = this.settings.spec;
				path = path.split('/');
				for (var p in path) {
					if (path == '') continue;
					var part = decodeURIComponent(path[p]);
					var re = new RegExp("\n\\s*" + part);
					var m = t.match(re);
					// we could not find the path part
					if (m == null) return;
					o += m.index;
					t = t.substr(m.index);
				}
				if (selectSubString === undefined) selectSubString = part.replace(/^[\n\s]*/, '').replace(/:$/, '');
				// the last mile: skip the whitespace
				var ws = t.indexOf(selectSubString);
				var s = o + ws;
				var e = s + selectSubString.length;
				textarea.setSelectionRange(s, e);
				textarea.focus();
				
				this.scrollToOffset(o);
			},
			scrollToOffset: function(o) {
				var t = this.settings.spec;
				t = t.substr(0, o);
				var lines = t.match(/\n/g).length;
				// TODO: it would be more elegant to create and destroy the helper on demand
				var helper = $('#specEditor .measureHelper').get(0)
				var lineHeight = $(helper).height();
				$('#specEditor textarea').scrollTop(lines * lineHeight - 100);
			},
			selectSchema: function(name) {
				return this.selectPath("components/schemas/" + name);
			},
			load: function() {
				// TODO: we want to store x and y (and maybe other data) with the document
				//       without violating the OAS schema. jsyaml does not read comments though..
				// ALSO: <<: blah - references are resolved during load and therefore lost on write.
				
				// if the document contains an inline viz, we restore the positions from there.
				var divs = this.settings.spec.match(/<div ([^>]*)>/g)
				for (d in divs) {
					var div = divs[d];
					try {
						var key = div.match(/oaie-key='([^']+)'/)[1];
						var x = div.match(/left:([0-9\.]+)px/)[1];
						var y = div.match(/top:([0-9\.]+)px/)[1];
						this.settings.positions[key] = { x: x, y: y };
					}
					catch (e) {
						//console.error("could not extract position info for", div, e);
					}
				}
				
				var data = jsyaml.load(this.settings.spec);
				console.log(data);
				app.spec = data;
				this.editorChanged = false;
				// do it again after vue has created the elements
				// TODO: rewrite to use vues nextTick
				Vue.nextTick(() => {
					connectArrowsAll();
					this.updateInlineViz();
				});
			},
			updateInlineViz: function() {
				var r = `<!--OAIE.viz--><div style='height:500px;background-color:#eee;overflow:auto;position:relative;white-space:nowrap;border-radius:10px;'>`;
				$('a.arrow').each((i,o) => {
					o = $(o);
					var borderStyle = o.hasClass('propertyArray') ? 'border-bottom:1px dashed black;' : 'border-bottom:1px solid black;'
					r += `<span style='${borderStyle}position:absolute;left:${o.css('left')};top:${o.css('top')};width:${o.css('width')};transform:${o.css('transform')};transform-origin:0 0;'><span style='border:1px solid black;width:5px;height:5px;position:absolute;right:0;transform:rotate(45deg);transform-origin:100% 0;border-left:0;border-bottom:0;'></span></span>`;
				});
				$('div.operation').each((i,o) => {
					o = $(o);
					var colorStyle = `border:1px solid ${o.css('border-color')};background:${o.css('background-color')};`;
					r += `<div oaie-key='${o.attr('oaie-key')}' style='${colorStyle}position:absolute;left:${o.css('left')};top:${o.css('top')};width:${o.width()}px;height:${o.height()}px;padding:5px;border-radius:5px;'>`;
					o.find('.path').each((i,o) => { r += `<div><b>${$(o).text()}</b></div>`; });
					o.find('.summary').each((i,o) => { r += `<div style='white-space:normal'>${$(o).text()}</div>`; });
					o.find('li span').each((i,o) => { r += `<div>${$(o).text().trim()}</div>`; });
					r += `</div>`;
				});
				$('div.schema').each((i,o) => {
					o = $(o);
					r += `<div oaie-key='${o.attr('oaie-key')}' style='position:absolute;left:${o.css('left')};top:${o.css('top')};width:${o.width()}px;height:${o.height()}px;border:1px solid silver;background:white;padding:5px;border-radius:5px;'>`;
					o.find('h1').each((i,o) => { r += `<div><b>${$(o).text()}</b></div>`; });
					o.find('li span').each((i,o) => { r += `<div>${$(o).text().trim()}</div>`; });
					r += `</div>`;
				});
				r += `</div>`;
				r += `<div style='padding:5px;color:gray;float:right;'>OAIE visualization</div><!--/OAIE.viz-->`;
				this.settings.spec = this.settings.spec.replace(/<!--OAIE.viz-->.*<!--\/OAIE.viz-->/, r);
				this.positionsChanged = false;
				this.save();
			},
			dump: function() {
				// TODO: this currently destroys comments and may destroy ordering, etc.
				//       is there a more elegant way?
				//       like walk the whole structure, monitor changes and only apply changes
				//       to parts of the input text..
				var yaml = jsyaml.dump(app.spec);
				console.log(yaml);
				
				//settings.spec = yaml;
			},
			addSchema: function() {
				var name = prompt("name");
				if (name === undefined) return;
				this.settings.spec += '    ' + name + ':\n      required:\n      properties:';
				this.settings.spec = settings.spec.replace(/\n\n/, '\n');
				this.load();
				// this.storeSettings();
			},
			addSchemaProperty: function(schema, schemaName) {
				var name = prompt("name");
				if (name === undefined) return;
				// TODO: this is all very hacky
				var s = this.settings.spec.indexOf(schemaName + ":");
				var e = this.settings.spec.substring(s).search(/\n    [^ ]/);
				if (e < 0) e = this.settings.spec.length;
				else e = e + s;
				var props = '\n      properties:';
				var p = this.settings.spec.indexOf(props, s);
				// add properties block if needed
				if (p < s || p > e) {
					this.settings.spec = this.settings.spec.slice(0, e) + props + '\n' + this.settings.spec.slice(e);
					p = e;
					e = e + props.length;
				}
				p = p + props.length;
				var type = 'string';
				var matchType = name.match(/\s+\(([^\)]+)\)/);
				if (matchType) {
					type = matchType[1];
					// TODO: if type is one of our defined types, build a ref instead of the type
					name = name.replace(/\s+\(([^\)]+)\)/, '');
				}
				var prop = '\n        ' + name + ':\n          type: ' + type + '\n';
				this.settings.spec = this.settings.spec.slice(0, e) + prop + this.settings.spec.slice(e);
				this.settings.spec = this.settings.spec.replace(/\n\n/, '\n');
				this.load();
				// TODO: select the new attribute
				// this.storeSettings();
			},
			moveAllY: function(minY, byY) {
				if (typeof minY == "object")
					minY = this.settings.positions[minY._key].y;
				for (var key in this.settings.positions) {
					var y = Number(this.settings.positions[key].y);
					if (y < minY - 10) continue;
					this.settings.positions[key].y = y + byY;
				}
				this.positionsChanged = true;
				settingsSystem.storeSettings();
				Vue.nextTick(() => {
					connectArrowsAll();
				});
			},
		},
		filters: {
			i18nMoustache: function(text) {
				text = text.replace(/\{\{ ?config.(\w*) ?\}\}/, function(m, g1) { return config[g1]; });
				return text;
			},
			i18nError: function(text) {
				if (ml[text] !== undefined)
					return ml[text];
				return text;
			},
		},
	});
	
	// TODO: actually we should do that on change of the spec (?)
	if (settingsSystem.settings)
		app.load();
	
	// TODO: remove this
	APP = app;
}

var config = {};
var i18n = {
	"en": {
		"localeName": "English",
		"localeCode": "en",
		
		"title": "OAIE",
	},
};

window.addEventListener('load', function() { init(config, i18n); });
</script>


<div style='position: fixed; right: 50px; bottom: 30px; font-size: 20pt;'>
	<span class='oaie'>sketch</span>
</div>

</body>
